package cz.ctu.guiproject.client.networking;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;

import android.util.Log;
import cz.ctu.guiproject.client.networking.listener.ServerListenerImpl;
import static cz.ctu.guiproject.client.resources.constants.Logger.*;
import static cz.ctu.guiproject.client.resources.constants.Time.*;

/**
 * This class is used to handle connection with the server. Provided interface
 * makes it possible to send messages to the server and all registered listeners
 * receives the appropriate event notification.
 * 
 * @author tomas.buk
 */
public final class ClientNetworkAgentImpl implements ClientNetworkAgent {

	// default output stream
	private ObjectOutputStream output;
	private Socket socket;
	// server address
	private String server;
	// server port number
	private int port;
	private ServerListenerImpl serverListener;
	// current state that indicates the whether the client is connected or not
	private State currentState;
	// observer pattern
	private ArrayList<ClientNetworkObserver> observers;
	// currently received message
	private Object currentMessage;

	/**
	 * Default contructor used to create client connected with server with
	 * particular address and port number.
	 * 
	 * @param server
	 * @param port
	 */
	public ClientNetworkAgentImpl(String server, int port) {
		this.server = server;
		this.port = port;
		observers = new ArrayList<ClientNetworkObserver>();
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				connectionFailed(true);
			}
		});
		t.start();
		try {
			t.join();
		} catch (InterruptedException e) {
			Log.e(TAG, e.getMessage());
		}
	}

	/**
	 * Used to report, that the connection was unsuccessful and failed.
	 * 
	 * @param reconnect
	 *            TODO
	 */
	public void connectionFailed(boolean reconnect) {
		if (serverListener != null) {
			serverListener.setRunning(false);
		}
		currentState = State.DISCONNECTED;
		if (reconnect) {
			reconnect();
		}
	}

	/**
	 * Attempts to reconnect each time specified by the interval
	 */
	void reconnect() {
		while (!start()) {
			Log.i(TAG, "Trying to connect to server...");
			sleep(RECONNECT_INTERVAL);
		}
		Log.i(TAG, "Succesfully connected to server!");
	}

	/**
	 * Starts the client, when the start is successful, returns true
	 * 
	 * @return true, if the start was successful
	 */
	private boolean start() {
		try {
			socket = new Socket(server, port);
		} catch (UnknownHostException ex) {
			Log.e(TAG, ex.getMessage());
			return false;
		} catch (IOException ex) {
			Log.e(TAG, ex.getMessage());
			return false;
		}

		Log.i(TAG, "Connection accepted " + socket.getInetAddress() + ": " + socket.getPort());

		// initialize output stream
		try {
			output = new ObjectOutputStream(socket.getOutputStream());
		} catch (IOException ex) {
			Log.e(TAG, ex.getMessage());
			return false;
		}

		// create new thread to listen for incomming messages
		serverListener = new ServerListenerImpl(this, socket);
		new Thread(serverListener).start();

		currentState = State.CONNECTED;
		// sucessfully connected to server
		return true;
	}

	/**
	 * Inner method to send a message to server
	 * 
	 * @param message
	 *            Message to be send to server
	 */
	private synchronized void sendMessage(Object message) {
		if (currentState != State.CONNECTED) {
			Log.e(TAG, "Client disconnected, unable to send messages.");
			// throw new RuntimeException(
			// "Client disconnected, unable to send messages.");
			return;
		}
		// try to send the message
		try {
			output.writeObject(message);
		} catch (IOException ex) {
			Log.e(TAG, "Exception occured when writing to server: " + ex);
			connectionFailed(true);
		}
	}

	@Override
	public void send(Object message) {
		sendMessage(message);
	}

	/**
	 * Makes the current thread sleep for specified amount of time.
	 * 
	 * @param time
	 *            Specified amount of time
	 */
	private void sleep(int time) {
		try {
			Thread.sleep(time);
		} catch (InterruptedException ex) {
			Log.e(TAG, ex.getMessage());
		}
	}

	/**
	 * Invoked from the ServerListener object to inform, that new message has
	 * been received.
	 * 
	 * @param currentMessage
	 */
	public void messageReceived(Object currentMessage) {
		this.currentMessage = currentMessage;
		notifyObservers();
	}

	@Override
	public void registerObserver(ClientNetworkObserver o) {
		// TODO kazdy clientObserver musi mit pretizeny metody equals a hashcode
		if (!observers.contains(o)) {
			observers.add(o);
		}
	}

	@Override
	public void removeObserver(ClientNetworkObserver o) {
		// TODO kazdy clientObserver musi mit pretizeny metody equals a hashcode
		observers.remove(o);
	}

	@Override
	public void notifyObservers() {
		for (ClientNetworkObserver observer : observers) {
			observer.update(currentMessage);
		}
	}

	@Override
	public void disableReconnect() {
		serverListener.disableReconnect();
		// currentState = State.DISCONNECTED;
	}
}
